﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Security.Cryptography;
using System.IO;
using System.Globalization;

namespace LockCrypt.Core {
    public class Utils {
        public static Random RandomSeed = new Random();

        /// <summary>
        /// Joins a list using a separator.
        /// </summary>
        /// <param name="list">The list to join.</param>
        /// <param name="separator">The separator to use.</param>
        /// <param name="trim">If set to <c>true</c>, trims whitespace from each index.</param>
        /// <param name="lastSeparator">The separator to use to join the second to last and last item, or <c>null</c> to use the separator. Eg: Could be "and".</param>
        /// <returns>A string containing the values separated by the separator</returns>
        public static string Implode(IEnumerable<string> list, string separator, bool trim, string lastSeparator) {
            StringBuilder buffer = new StringBuilder();
            int num = list.Count(),
                added = 0;
            foreach(string item in list) {
                if(trim) {
                    buffer.Append(item.Trim());
                } else {
                    buffer.Append(item);
                }
                if(added + 1 < num) {
                    if(added == num - 2 && lastSeparator != null) {
                        buffer.Append(lastSeparator);
                    } else {
                        buffer.Append(separator);
                    }
                }
                added++;
            }
            return buffer.ToString();
        }

        /// <summary>
        /// Generates a (hopefully) unique ID 10 characters long using a combination of 53 (non-HTML safe) or 39 (HTML safe) characters.
        /// There are 3.9e11 combinations if the ID does not need to be HTML safe, otherwise there are just over 5.3 billion.
        /// </summary>
        /// <param name="HTMLSafe"><c>true</c> if the generated ID must be valid in HTML without encoding.</param>
        /// <returns>The generated ID</returns>
        public static string GenID(bool HTMLSafe) {
            return GenID(HTMLSafe, 10);
        }

        /// <summary>
        /// Generates a (hopefully) unique ID using a combination of 53 (non-HTML safe) or 39 (HTML safe) characters.
        /// </summary>
        /// <param name="HTMLSafe"><c>true</c> if the generated ID must be valid in HTML without encoding.</param>
        /// <param name="length">The length of the ID in characters.</param>
        /// <returns>The generated ID</returns>
        public static string GenID(bool HTMLSafe, int length) {
            StringBuilder bld = new StringBuilder();
            if(HTMLSafe) {
                string possible = "ABCEDFGHIJKLMNOPQRSTUVWXYZ0123456789~_-",
                       firstPossible = "ABCEDFGHIJKLMNOPQRSTUVWXYZ"; // make sure the first character isn't a number, which invalidates xhtml
                bld.Append(possible[RandomSeed.Next(firstPossible.Length - 1)]);
                for(int i = 1; i < length; i++) {
                    bld.Append(possible[RandomSeed.Next(possible.Length - 1)]);
                }
            } else {
                string possible = "ABCEDFGHIJKLMNOPQRSTUVWXYZ0123456789%£$!*/#~][{}|=_-^";
                for(int i = 0; i < length; i++) {
                    bld.Append(possible[RandomSeed.Next(possible.Length - 1)]);
                }
            }
            return bld.ToString();
        }

        /// <summary>
        /// Formats filesizes to a more user-readable format by adding B/KB/MB/GB etc
        /// </summary>
        /// <param name="bytes">The file-size in bytes.</param>
        /// <returns>A string containing the formatted size (Eg: 1MB, 74KB)</returns>
        public static string PrettyFileSize(long bytes) {
            if(bytes == 0) {
                return "0 Bytes";
            } else if(bytes < 1024) {
                return bytes + "B";
            } else if(bytes < 1048576) {
                return Math.Round((double)(bytes / 1024)) + "KB";
            } else if(bytes < 1073741824) {
                return Math.Round((double)(bytes / 1048576), 2) + "MB";
            } else if(bytes < 1099511627776) {
                return Math.Round((double)(bytes / 1073741824)) + "TB";
            } else {
                return "Too damn big";
            }
        }

        /// <summary>
        /// Retrieves the date the assembly was compiled.
        /// </summary>
        /// <returns>The date the assembly was compiled.</returns>
        public static DateTime RetrieveLinkerTimestamp() {
            string filePath = System.Reflection.Assembly.GetCallingAssembly().Location;
            const int c_PeHeaderOffset = 60;
            const int c_LinkerTimestampOffset = 8;
            byte[] b = new byte[2048];
            System.IO.Stream s = null;
            try {
                s = new System.IO.FileStream(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read);
                s.Read(b, 0, 2048);
            } finally {
                if(s != null) {
                    s.Close();
                }
            }
            int i = System.BitConverter.ToInt32(b, c_PeHeaderOffset);
            int secondsSince1970 = System.BitConverter.ToInt32(b, i + c_LinkerTimestampOffset);
            DateTime dt = new DateTime(1970, 1, 1, 0, 0, 0);
            dt = dt.AddSeconds(secondsSince1970);
            dt = dt.AddHours(TimeZone.CurrentTimeZone.GetUtcOffset(dt).Hours);
            return dt;
        }

        /// <summary>
        /// Gets the path of the directory in which the application is located.
        /// </summary>
        /// <returns>The path of the directory in which the application is located.</returns>
        public static string GetApplicationPath() {
            string fqPath = System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0].FullyQualifiedName;
            return Path.GetDirectoryName(fqPath);
        }

        /// <summary>
        /// Find the first unique index of a string in a collection of strings. The incrementation starts at 2.
        /// </summary>
        /// <param name="name">The name.</param>
        /// <param name="otherNames">The other names.</param>
        /// <returns>The first unique index of a string in a collection of strings</returns>
        /// <example>
        /// Collection contains - name, name1, name2. 
        /// "name3" will be returned.
        /// </example>
        public static string GetNextAvailableName(string name, IEnumerable<string> otherNames) {
            if(!otherNames.Contains(name)) {
                return name;
            } else {
                bool gotName = false;
                string currentName = name;
                for(int i = 2; !gotName; ) {
                    currentName = name + ' ' + i;
                    if(otherNames.Contains(currentName)) {
                        i++;
                    } else {
                        name = currentName;
                        gotName = true;
                    }
                }
                return name;
            }
        }

        /// <summary>
        /// Gets the ISO 3166 code of the specified culture.
        /// </summary>
        /// <param name="info">The CultureInfo representing the culture.</param>
        /// <returns>An ISO3166 code to reresent a country which uses the CultureInfo specified</returns>
        public static string GetISO3166(CultureInfo info) {
            if(info.IsNeutralCulture) {
                if(info.TwoLetterISOLanguageName == "zh") { // chinese is awkward
                    return "cn";
                } else {
                    return new RegionInfo(CultureInfo.CreateSpecificCulture(info.Name).LCID).TwoLetterISORegionName;
                }
            } else {
                return new RegionInfo(info.Name).TwoLetterISORegionName;
            }
        }

        /// <summary>
        /// Reads the contents of the stream into a byte array.
        /// data is returned as a byte array. An IOException is
        /// thrown if any of the underlying IO calls fail.
        /// </summary>
        /// <param name="source">The source.</param>
        /// <returns>
        /// A byte array containing the contents of the stream.
        /// </returns>
        /// <exception cref="NotSupportedException">The stream does not support reading.</exception>
        /// <exception cref="ObjectDisposedException">Methods were called after the stream was closed.</exception>
        /// <exception cref="System.IO.IOException">An I/O error occurs.</exception>
        public static byte[] ReadAllBytes(Stream stream, int initialLength) {
            // If we've been passed an unhelpful initial length, just use 32K.
            if(initialLength < 1) {
                initialLength = 32768;
            }

            byte[] buffer = new byte[initialLength];
            int read = 0;
            int chunk;
            while((chunk = stream.Read(buffer, read, buffer.Length - read)) > 0) {
                read += chunk;

                // If we've reached the end of our buffer, check to see if there's any more information
                if(read == buffer.Length) {
                    int nextByte = stream.ReadByte();

                    // End of stream? If so, we're done
                    if(nextByte == -1) {
                        return buffer;
                    }
                    // Nope. Resize the buffer, put in the byte we've just read, and continue
                    byte[] newBuffer = new byte[buffer.Length * 2];
                    Array.Copy(buffer, newBuffer, buffer.Length);
                    newBuffer[read] = (byte)nextByte;
                    buffer = newBuffer;
                    read++;
                }
            }
            // Buffer is now too big. Shrink it.
            byte[] ret = new byte[read];
            Array.Copy(buffer, ret, read);
            return ret;
        }

        /// <summary>
        /// Gets scaled dimensions for an image.
        /// </summary>
        /// <param name="maxWidth">The maximum allowed width of the image.</param>
        /// <param name="maxHeight">The maximum allowed height of the image.</param>
        /// <param name="originalWidth">The original Width.</param>
        /// <param name="originalHeight">The original Height.</param>
        /// <returns>
        /// The scaled dimensions if the image was larger than the maximum size.
        /// </returns>
        public static Size CalculateDimensions(int maxWidth, int maxHeight, int originalWidth, int originalHeight) {
            int width = originalWidth,
                height = originalHeight;
            if(height <= maxHeight && width <= maxWidth)
                return new Size(width, height);
            float Multiplier = (float)((float)maxWidth / (float)width);
            if((height * Multiplier) <= maxHeight) {
                height = (int)(height * Multiplier);
                return new Size(maxWidth, height);
            }
            Multiplier = (float)maxHeight / (float)height;
            width = (int)(width * Multiplier);
            return new Size(width, maxHeight);
        }

        public struct Size {
            public int Width;
            public int Height;
            public Size(int width, int height) {
                Width = width;
                Height = height;
            }
        }

        /// <summary>
        /// Reverses a string.
        /// </summary>
        /// <param name="stringToReverse">The string to reverse.</param>
        /// <returns>The string in opposite order.</returns>
        public static string ReverseString(string stringToReverse) {
            char[] characters = stringToReverse.ToCharArray();
            Array.Reverse(characters);
            return new string(characters);
        }
    }
}